Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

21.Gün - SwiftUI Proje-2 Bölüm-2

Bu bölümde, 1 .bölümde üzerinde çalıştığımız Stack, Color, Frame, Gradient, Button, Image,Alert gibi konuları kullanarak GuessTheFlag uygulamamızı inşa ediyoruz. Uygulamamızı çalıştırıyor ve adım adım kullanıcı arayüzünü geliştiriyoruz.

Bu proje aynı zamanda GitHub’da da bulunmaktadır.

GitHub - GorkemGuray/GuessTheFlag: 100 Days of SwiftUI - Project-2

Uygulamamıza, kullanıcıya ne yapacağını söyleyen iki etiket ve ardından üç dünya bayrağını gösteren üç resim düğmesinden oluşan temel UI yapısını oluşturarak başlayacağız.

Buradan bayrak resimlerini indirebilirsiniz.

Butonların Hizalanması #

İndirdiğiniz bu dosyaları asset klasörüne sürükleyin. Görsellerin @2x veya @3x şeklinde adlandırıldığını fark edeceksiniz. Bunlar, farklı iOS ekran türlerini işlemek için iki veya üç kat çözünürlüklü görsellerdir.

Oyun verilerini saklamak için iki property’ye ihtiyacımız olacak:

  • Oyunda göstermek istediğimiz tüm ülke resimlerinin bir array’i
  • Hangi ülkenin resimlerinin olduğunu saklayan bir integer
var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Spain", "UK", "Ukraine", "US"]
var correctAnswer = Int.random(in: 0...2)

Int.random(in:) methodu rastgele bir sayı verir. Hangi ülke bayrağına dokunulacağına karar vermek için bunu kullanacağız.

body içerisinde prompt’u dikey bir stack olarak yerleştirmemiz gerekiyor, bununla başlayalım;

var body: some View {
    VStack {
        Text("Tap the flag of")
        Text(countries[correctAnswer])
    }
}

Bunun altında dokunulabilir bayrak butonlarımız olsun istiyoruz. Bunları aynı VStack ’e ekleyebilsek de aslında ikinci bir VStack oluşturabiliriz, böylece aralıklar üzerinde daha fazla kontrole sahip oluruz.

Yukarıda oluşturduğumuz VStack iki text view içeriyor ve boşluk yok, ancak bayraklar arasında 30pt boşluk bırakarak daha iyi bir görünüm elde edeceğiz.

Bu yüzden, önceki VStack’imizi bu kez 30 spacing değerine sahip başka bir VStack ile saracağız ve ardından yeni bir ForEach döngüsü ekleyeceğiz.

VStack(spacing: 30) {
    VStack {
        Text("Tap the flag of")
        Text(countries[correctAnswer])
    }

    ForEach(0..<3) { number in
        Button {
           // flag was tapped
        } label: {
            Image(countries[number])
        }
    }
}

Bu şekilde iki dikey stack olması, nesneleri daha hassas bir şekilde konumlandırmamızı sağlar: dış stack, görünümlerini 30pt aralıklarla yerleştirirken, iç yığının özel bir aralığı yoktur.

swiftui guesstheflag first ui

Bu görünüm her ne kadar bir fikir verse de çok kullanıcı dostu değil.

Şimdilik bayrakların daha kolay görünebilmeleri için mavi bir arka plan koyalım. Bu VStack’in arkasına bir şey koymak anlamına geldiğinden, bir ZStack kullanmamız gerekiyor.

Dış VStack’in etrafına aşağıdaki gibi bir ZStack koyarak başlayalım.

var body: some View {
    ZStack {
        // önceki VStack kodu
    }
}

Bunu dıştaki VStack’in hemen öncesine koyalım;

Color.blue
    .ignoresSafeArea()

Buradaki .ignoresSafeArea() modifier’ı, rengin ekranın kenarına kadar gitmesini sağlar.

Daha koyu bir arka planımız olduğundan, metnin daha iyi öne çıkması için daha parlak bir renk vermeliyiz.

Text("Tap the flag of")
    .foregroundStyle(.white)

Text(countries[correctAnswer])
    .foregroundStyle(.white)

Oyuncu Skorunun Alert ile Gösterilmesi #

Oyunun işlevini yerine getirmesi için, bayrakların gösterilme sırasını rastgele hale getirmemiz, bir bayrağa dokunulduğunda doğru ya da yanlış olduklarını söyleyen bir uyarı tetiklememiz ve ardından bayrakları yeniden karıştırmamız gerekiyor.

correctAnswer ’ı zaten rastgele bir tamsayıya ayarladık, ancak bayraklar her zaman aynı sırada başlıyor. Bunu düzeltmek için oyun başladığında countries array’ini karıştırmamız gerekiyor, bu sebeple şu şekilde bir değişiklik yapabiliriz;

var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Spain", "UK", "Ukraine", "US"].shuffled()

Bu sayede shuffled() methodu ile array sırası otomatik olarak rastgele hale geliyor.

Peki bir bayrağa dokunulduğunda ne yapmalıyız? // flag was tapped yorumunu , doğru bayrağa dokunulup dokunulmadığını belirleyen bir kodla değiştirmemiz gerekiyor. Bunu yapmanın en iyi yolu ise butonun tamsayısını kabul eden ve bunun correctAnswer property’imiz ile eşleşip eşleşmediğini kontrol eden yeni bir method’tur.

Kullanıcıların ilerlemelerini takip edebilmeleri için kullanıcıya ne olduğunu belirten bir alert göstermek istiyoruz. Bu yüzden, uyarının gösterilip gösterilmediğini saklamak için şu property’yi ekleyelim;

@State private var showingScore = false

Ve alert’in içinde gösterilecek başlığı saklamak için şu property’yi ekleyelim;

@State private var scoreTitle = ""

Dokunulan butonun numarasını kabul edecek, bunu doğru yanıtla karşılaştıracak ve ardından anlamlı bir alert gösterebilmemiz için az önce tanımladığımız iki property’yi (showingScore ve scoreTitle) ayarlayacak bir methoda ihtiyacımız var.

body property’sinden sonra şunu ekleyelim;

func flagTapped(_ number: Int) {
    if number == correctAnswer {
        scoreTitle = "Correct"
    } else {
        scoreTitle = "Wrong"
    }

    showingScore = true
}

Şimdi // flag was tapped yorumu yerine methodumuzu yerleştirebiliriz.

flagTapped(number)

number bize ForEach tarafından verilmektedir, bu sebeple flagTapped() methodu içinde doğrudan kullanabiliriz.

alert kapatıldığında ülkeleri yeniden karıştırarak ve yeni bir doğru cevap seçerek oyunu sıfırlayan askQuestion() methodunu yazacağız.

func askQuestion() {
    countries.shuffle()
    correctAnswer = Int.random(in: 0...2)
}

Bu kod derlenmeyecek ve hata verecektir, view’ın @State ile işaretlenmemiş property’lerini değiştirmeye çalışıyoruz ve buna izin verilmiyor. Bu sebeple, countries ve correctAnswer ’ın bildirildiği yere giderek önlerine @State private eklememiz gerekmektedir.

@State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Spain", "UK", "Ukraine", "US"].shuffled()
@State private var correctAnswer = Int.random(in: 0...2)

Artık alert’i göstermeye hazırız. Bunun için yapılacaklar;

  1. alert() modifier’ı kullanılacak, böylece showingScore doğru olduğunda uyarı gösterilecek.
  2. scoreTitle ’da belirlediğimiz başlık gösterilecek.
  3. Dokunulduğunda askQuestion() methodunu çağıran ve alerti kapatan bir butonumuz olacak.

body içerisinde bulunan ZStack ’in sonuna aşağıdaki kodu yerleştirelim;

.alert(scoreTitle, isPresented: $showingScore) {
    Button("Continue", action: askQuestion)
} message: {
    Text("Your score is ???")
}

Bayrakları Şekillendirmek #

Oyunumuz score haricinde çalışıyor fakat görünümümüzü biraz daha iyi hale getirebiliriz.

İlk olarak, düz mavi arka plan rengini maviden siyaha linear gradient ile değiştirelim, bu sayede bayrağımızda mavi bir şerit olsa bile arka plandan ayrışmasını sağlar.

Şu satırı bulun;

Color.blue
    .ignoresSafeArea()

Ve şu şekilde değiştirin;

LinearGradient(colors: [.blue, .black], startPoint: .top, endPoint: .bottom)
    .ignoresSafeArea()

gradient ui

Şimdi kullandığımız yazı stillerini değiştirelim;

iOS’ta yerleşik yazı tipi boyutlarından birini seçmemizi sağlayan font() modifier’ını kullanarak metnin boyutunu ve stilini kontrol edebiliriz. Yazı tiplerinin kalınlığını (weight) ise weight() modifier’ı ile kontrol edebiliriz.

Bunların her ikisini de kullanarak başlayalım, “Tap the flag of” metninden hemen sonra ekleyelim;

.font(.subheadline.weight(.heavy))

Text(countries[correctAnswer]) view’ından sonra ise şunları ekleyelim;

.font(.largeTitle.weight(.semibold))

“Large title” iOS’un bize sunduğu en büyük yerleşik yazı tipi boyutudur ve kullanıcının yazı tipleri için yaptığı ayara bağlı olarak otomatik olarak yukarı veya aşağı ölçeklenir. Dynamic Type olarak da bilinmektedir.

Bayrakları biraz daha hareketlendirmek için şeklini değiştirip biraz da gölge ekleyelim.

Swift’te yerleşik dört şekil vardır: dikdörtgen (rectangle), yuvarlatılmış dikdörtgen (rounded rectangle), daire (circle) ve kapsül (capsule). Burada kapsülü kullancağız. Resmimizi kapsül şeklinde yapmak için aşağıdaki gibi .clipShape(.capsule) modifier’ını eklemek yeterlidir.

.clipShape(.capsule)

Ayrıca her bayrağın etrafına bir gölge efekti uygulayarak onları arka plandan ayrışmasını sağlamak istiyoruz. Gölgenin rengini, yarıçapını, X ve Y ofsetini parametre alan shadow() modifier’ı kullanılarak yapılır. Burada sadece yarıçapı belirtebiliriz bu durumda diğer parametreler varsayılan değeleri almaktadır.

.shadow(radius: 5)

Böylece bunların hepsinin eklendiği bayrak resmimiz şöyle görünür;

Image(countries[number])
    .clipShape(.capsule)
    .shadow(radius: 5)

finished ui styling

Dizaynın Geliştirilmesi #

Bu noktada uygulamayı inşa ettik ve iyi çalışıyor, fakat uygulamayı daha iyi bir hale getirebiliriz.

Bayrakların arkasındaki mavi-siyah gradient ile başlayalım. Bunu stop’lu gradient ile yapabiliriz. Birbirinin aynısı olan iki stop oluşturursak, gradient tamamen ortadan kalkar renk doğrudan birinden diğerine geçer.

RadialGradient(stops: [
    .init(color: .blue, location: 0.3),
    .init(color: .red, location: 0.3),
], center: .top, startRadius: 200, endRadius: 700)
    .ignoresSafeArea()

radial gradient with same stop

Bu ilginç bir etki, sanki kırmızı bir arka planın üzerine mavi bir daire yerleştirmişiz gibi. Ama aynı zamanda da çirkin 😅 kırmızı ve mavi renkler birlikte çok parlak.

Bu sebeple daha uyumlu görünen bir şey elde etmek için aynı renklerin tonlanmış veriyonlarını kullanabiliriz.

RadialGradient(stops: [
    .init(color: Color(red: 0.1, green: 0.2, blue: 0.45), location: 0.3),
    .init(color: Color(red: 0.76, green: 0.15, blue: 0.26), location: 0.3),
], center: .top, startRadius: 200, endRadius: 400)
    .ignoresSafeArea()

radial gradient new colors

Ardından 30pt spacing’e sahip VStack’i 15pt’ye düşürelim;

VStack(spacing: 15) {

Bunu yapıyoruz çünkü, bayrak ve text’lerden oluşan alanın tamamını renkli bir çerçeve içine alacağız. Bunu yapmak için aynı VStack’in sonuna aşağıdakileri ekleyin.

.frame(maxWidth: .infinity)
.padding(.vertical, 20)
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 20))

VStack rectangle

Şimdiden iyi görünüyor !

Yeni bir VStack ekleyerek, ana kutumuzdan önce bir başlık ve bir score alanı gösterelim.

Bunu eklemek için, mevcut VStack ’i üst kısmında bir başlık olan yeni bir VStack ile saralım.

VStack {
    Text("Guess the Flag")
        .font(.largeTitle.weight(.bold))
        .foregroundStyle(.white)

    // current VStack(spacing: 15) code
}

bold kullanmak o kadar yaygındır ki bunun için bir kısayol var: .font(.largeTitle.bold())

Ayrıca başlığımızın altına bir de score alanı ekleyelim.

Text("Score: ???")
    .foregroundStyle(.white)
    .font(.title.bold())

new vstack

“Guess the Flag” ve score etiketleri gayet güzel görünüyor. Fakat kutumuzun içindeki yazıların okunması biraz zor.

Bunu düzeltmek için, Text(countries[correctAnswer]) ’da bulunan foregroundStyle() modifier’ını silebiliriz, böylece varsayılan olarak sistem için primary rengi kullanır.

“Tap the flag of” ifadesine gelince bunun renginin biraz parlamasını istiyoruz bu sebeple foregroundStyle() modifier’ını şu şekilde değiştirebiliriz.

.foregroundStyle(.secondary)

text color adjustment

Arayüzümüz gayet iyi çalışıyor fakat büyük ekranlı cihazlar için bütün öğeler merkeze toplanmış gibi gözüküyor. Altta ve üstte boşluklar var. Spacer() kullanarak bu boşlukları dağıtabiliriz ayrıca küçük ekranlı cihazlar için de uyumlu hale getirebiliriz.

Bu problemi çözmek için en dıştaki VStack’e Spacer() ekleyeceğiz. Ekleyeceğimiz 4 adet Spacer() olacak;

  • Bir tane “Guess the Flag” yazısından önce
  • İki tane “Score: ???” yazısından önce
  • Bir tane “Score: ???” yazısından sonra
VStack{
                Spacer()
                Text("Guess the Flag")
                    .font(.largeTitle.weight(.bold))
                    .foregroundStyle(.white)
                Spacer()
                Spacer()
                Text("Score: ???")
                    .foregroundStyle(.white)
                    .font(.title.bold())
                Spacer()
// rest of code

}

ve son olarak en dıştaki VStack ’e padding() ekleyelim.

.padding()

Bu sayede kodumuz küçük ekranlı cihazlardan büyük ekranlı cihazlara kadar hepsinde düzgünce görünecektir.

Final Design


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 21 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.